package se.liu.oschi129.world.objects;

import se.liu.oschi129.animation.Animation;
import se.liu.oschi129.shapefactory.ShapeFactory;
import se.liu.oschi129.world.model.World;
import se.liu.oschi129.world.model.WorldGraphics;

import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

/**
 * All instances of this class (or subclasses) are objects that can be added to
 * a world. Specific objects such as enemies or solid blocks are subclasses
 * of WorldObject. 
 * 
 * @author oschi129
 */

public abstract class WorldObject {
        
        /** A world object can be any of these types:
         *              SOLID - An object that can't be broken or penetrated (such as ground)
         *              PLATFORM - A solid object that can be jumped onto from below
         *              SOFT - An object that can penetrated without any interaction (i.e. some enemies such as fireballs) 
         */
        public enum Type {SOLID, PLATFORM, SOFT};
        private Type type;
        
        // The position of the object
        private Point2D.Double position;
        
        // The width and height of the object
        private double width, height;
        
        // The mask of the object and its factory
        private Shape mask;
        private ShapeFactory maskFactory = new DefaultMask();
        
        // The animation used for the object and the subimage that is being shown
        private Animation animation;
        private double animationIndex, animationSpeed;
        
        // Whatever or not the object is visible
        private boolean visible;
        
        // The depth of the object (drawing order) - A high depth means the object should be drawn in front of low depth objects
        private int depth;
        
        // The id of an object - the value increases by 1 every time you create an object
        private static int nextId = 0;
        private int id;
        
        /**
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 * - - - - - - - - - - - - Constructors - - - - - - - - - - - 
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 */
        
        public WorldObject(Type type, Animation animation, boolean visible, double x, double y, double width, double height) { 
                this(type, x, y, width, height);
                this.visible = visible;
                this.animation = animation;
        }
        
        public WorldObject(Type type, double x, double y, double width, double height) { 
                this(x, y, width, height);
                this.type = type;
        }
        
        public WorldObject(double x, double y, double width, double height) { 
                position = new Point2D.Double(x, y);
                this.width = width;
                this.height = height;
                updateMask();
                setVisible(false);
                setType(Type.SOFT);
                setImageSpeed(1);
                setDepth(0);
                animationIndex = 0;
                id = nextId;
                nextId++;
        }
        
        /**
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 * - - - - - - - - - - - Public methods - - - - - - - - - -
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 */
        
        /** Returns the id of the object */
        public int getId() {
        	return id;
        }
        
        /** Returns the mask of the object */
        public Shape getMask() {
        	return mask;
        }
        
        public void setMaskFactory(ShapeFactory maskFactory) {
        	this.maskFactory = maskFactory;
        }
        
        /** Returns the position of the object */
        public Point2D.Double getPosition() {
        	return new Point2D.Double(position.getX(), position.getY());
        }

        /** Sets the position of the object */
        public void setPosition(Point2D.Double position) {
        	this.position = position;
        	updateMask();
        }
        
        /** Returns the x-coordinate of the object */
        public double getX() {
                return position.getX();
        }
        
        /** Sets the x-coordinate of the object */
        public void setX(double x) {
                position.x = x;
                updateMask();
        }
        
        /** Returns the y-position of the object */
        public double getY() {
                return position.getY();
        }
        
        /** Sets the y-position of the object */
        public void setY(double y) {
                position.y = y;
                updateMask();
        }
        
        /** Returns the width of the object */
        public double getWidth() {
                return width;
        }
        
        /** Sets the width of the object */
        public void setWidth(double width) {
                this.width = width;
                updateMask();
        }
        
        /** Returns the height of the object */
        public double getHeight() {
                return height;
        }
        
        /** Sets the height of the object */
        public void setHeight(double height) {
                this.height = height;
                updateMask();
        }
        
        /** Returns whatever the object is visible or not */
        public boolean getVisible() {
                return visible;
        }
        
        /** Sets whatever the object should be visible or not */
        public void setVisible(boolean visible) {
                this.visible = visible;
        }
        
        /** Returns the depth of the object */
        public int getDepth() {
                return depth;
        }
        
        /** Returns the animation of the object */
        public Animation getAnimation() {
                return animation;
        }
        
        /** Sets the animation of the object */
        public void setAnimation(Animation animation) {
        	if (this.animation != animation) {
        		this.animation = animation;
        		setImageIndex(0);
        	}
        }
        
        /** Returns the type of the object */
        public Type getType() {
        	return type;
        }
        
        /** Sets the type of the object (type.ALL cannot be set to an object) */
        public void setType(Type type) {
        	this.type = type;
        }
        
        /** Returns the current subimage used for the object */
        public double getImageIndex() {
                return animationIndex;
        }
        
        /** Sets the current subimage of the object */
        public void setImageIndex(double index) {
                animationIndex = index % animation.length();
        }
        
        /** Returns the animation speed of the object (default = 1 image/step) */
        public double getImageSpeed() {
                return animationSpeed;
        }
        
        /** Sets the animation speed of the object (default = 1 image/step) */
        public void setImageSpeed(double speed) {
                animationSpeed = speed;
        }
        
        /** Returns true if the object is movable, otherwise false */
        public boolean isMovable() {
        	return false;
        }
        
        /** Returns true if a collision occurs between the object and obj */
        public boolean collision(WorldObject obj) {
                return mask.intersects(obj.getMask().getBounds2D());
        }
        
        /** Returns true if the object intersects the given rectangle */
        public boolean intersects(Rectangle2D rect) {
        	return mask.intersects(rect);
        }
        
        /** Returns true if the coordinate (x,y) intersects the object */
        public boolean contains(double x, double y) {
               return mask.contains(x, y);
        }
        
        /** Returns true if the object is on top of obj */
        public boolean onTopOf(WorldObject obj) {
        	 if (getX() + getWidth()/2 > obj.getX() && getX() + getWidth()/2 <= obj.getX() + obj.getWidth()) {
        		 double diff = obj.getY() - getY() - getHeight();
        		 // Allow a small differential - calculations may produce a small error
        		 return (0 <= diff && diff < 2);
        	 }
        	 else
        		 return false;
        }
        
        /** Return true if the object was hit from below by obj */
        public boolean hitFromBelow(WorldMovableObject obj) {
        	Point2D.Double position = obj.getPosition();
        	obj.setY(obj.getY() + obj.getVerticalSpeed());
        	obj.setX(obj.getX() + obj.getHorizontalSpeed());
        	if (collision(obj) && obj.getVerticalSpeed() < 0) {
        		obj.setPosition(position);
        		return true;
        	} else {
        		obj.setPosition(position);
        		return false;
        	}
        }
        
        /** Returns true if other is another object */
        public boolean isOther(WorldObject other) {
        	return this != other;
        }
        
        /** Performs a step action for the object */
        public void performStepAction(World world) {
        		if (getAnimation() != null)
        			setImageIndex(getImageIndex() + getImageSpeed());
        }
        
        /** Performs a destruction action for the object */
        public void performDestructionAction(World world) {}
        
        /** This action is performed when a world object is hit */
        public void performHitAction(World world) {}
        
        /** Performs a drawing action for the object 
         * IMPORTANT! This action is NOT meant to update any values to the object.
         * This would violate the paradigm of the WorldObject hierarchy! Override with care! 
         * Furthermore - if you change the Graphics2D object, make sure to reset the change! */
        public void performDrawingAction(WorldGraphics worldg) {
        	worldg.drawAnimation(getX(), getY(), getWidth(), getHeight(), animation, (int) getImageIndex());
        }
        
        /**
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 * - - - - - - - - - - Protected methods - - - - - - - -
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 */
        
        /** Sets whatever the object should be visible or not
         * IMPORTANT! This method is protected and may only
         * be used within the constructor! */
        protected void setDepth(int depth) {
                this.depth = depth;
        }
        
        /**
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 * - - - - - - - - - - Private methods - - - - - - - - - -
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 */
        
        /** Updates the mask of the object */
        private void updateMask() {
        	mask = maskFactory.makeShape(getX(), getY(), getWidth(), getHeight());
        }
        
        /**
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 * - - - - - - - - - - Private classes - - - - - - - - - - -
    	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    	 */
        
        /** This class creates a default mask for a world object */
        private class DefaultMask implements ShapeFactory {
        	
        	public Shape makeShape(double x, double y, double width, double height) {
        		return new Rectangle2D.Double(x,y,width,height);
        	}
        	
        }

}
